// SPDX-FileCopyrightText: 2020 k0s authors
// SPDX-License-Identifier: Apache-2.0

package token

import (
	"bytes"
	"context"
	"fmt"
	"slices"
	"time"

	k8sutil "github.com/k0sproject/k0s/pkg/kubernetes"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/client-go/kubernetes"
	tokenutil "k8s.io/cluster-bootstrap/token/util"
	bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1"

	"github.com/sirupsen/logrus"
)

type Token struct {
	ID     string
	Role   string
	Expiry string
}

func (t Token) ToArray() []string {
	return []string{t.ID, t.Role, t.Expiry}
}

// NewManager creates a new token manager using given kubeconfig
func NewManager(kubeconfig string) (*Manager, error) {
	logrus.Debugf("loading kubeconfig from: %s", kubeconfig)
	client, err := k8sutil.NewClientFromFile(kubeconfig)
	if err != nil {
		return nil, err
	}
	return &Manager{
		client: client,
	}, nil
}

// NewManagerForClient creates a new token manager using given client
func NewManagerForClient(client kubernetes.Interface) (*Manager, error) {
	return &Manager{
		client: client,
	}, nil
}

// Manager is responsible to manage the join tokens in kube API as secrets in kube-system namespace
type Manager struct {
	client kubernetes.Interface
}

func RandomBootstrapSecret(role string, ttl time.Duration) (*corev1.Secret, *bootstraptokenv1.BootstrapTokenString, error) {
	token := bootstraptokenv1.BootstrapToken{
		TTL: &metav1.Duration{Duration: ttl},
	}

	var err error
	token.Token, err = generateBootstrapToken()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to generate bootstrap token: %w", err)
	}

	var legacyUsages []string // legacy usages for backwards compatibility

	switch role {
	case RoleWorker:
		token.Description = "Worker bootstrap token generated by k0s"
		token.Usages = append(token.Usages, "authentication")
	case RoleController:
		token.Description = "Controller bootstrap token generated by k0s"
		token.Usages = append(token.Usages, "controller-join")
		legacyUsages = append(legacyUsages, "controller-join")
	default:
		return nil, nil, fmt.Errorf("unsupported role %q", role)
	}

	secret := bootstraptokenv1.BootstrapTokenToSecret(&token)
	for _, usage := range legacyUsages {
		// Add the usages also in their legacy form.
		secret.Data["usage-"+usage] = []byte("true")
	}

	return secret, token.Token, nil
}

// Create creates a new bootstrap token
func (m *Manager) Create(ctx context.Context, valid time.Duration, role string) (*bootstraptokenv1.BootstrapTokenString, error) {
	secret, token, err := RandomBootstrapSecret(role, valid)
	if err != nil {
		return nil, err
	}

	_, err = m.client.CoreV1().Secrets(metav1.NamespaceSystem).Create(ctx, secret, metav1.CreateOptions{})
	if err != nil {
		return nil, err
	}

	return token, nil
}

// List returns all the join tokens.
func (m *Manager) List(ctx context.Context) (tokens []Token, _ error) {
	secrets, err := m.client.CoreV1().Secrets(metav1.NamespaceSystem).List(ctx, metav1.ListOptions{
		FieldSelector: fields.OneTermEqualSelector("type", string(corev1.SecretTypeBootstrapToken)).String(),
	})
	if err != nil {
		return nil, err
	}

	for _, secret := range secrets.Items {
		parsed, err := bootstraptokenv1.BootstrapTokenFromSecret(&secret)
		if err != nil {
			continue // ignore invalid tokens
		}

		token := Token{ID: parsed.Token.ID}

		if slices.Contains(parsed.Usages, "controller-join") {
			token.Role = "controller"
		} else if bytes.Equal(secret.Data["usage-controller-join"], []byte("true")) {
			// Legacy form of token usage
			token.Role = "controller"
		} else if slices.Contains(parsed.Usages, "authentication") {
			token.Role = "worker"
		}

		if parsed.Expires != nil {
			token.Expiry = parsed.Expires.UTC().Format(time.RFC3339)
		}

		tokens = append(tokens, token)
	}

	return tokens, nil
}

func (m *Manager) Remove(ctx context.Context, tokenID string) error {
	err := m.client.CoreV1().Secrets(metav1.NamespaceSystem).Delete(ctx, tokenutil.BootstrapTokenSecretName(tokenID), metav1.DeleteOptions{})
	if apierrors.IsNotFound(err) {
		return nil
	}
	return err
}

// Generates a new, random Bootstrap Token.
func generateBootstrapToken() (*bootstraptokenv1.BootstrapTokenString, error) {
	token, err := tokenutil.GenerateBootstrapToken()
	if err != nil {
		return nil, err
	}

	return bootstraptokenv1.NewBootstrapTokenString(token)
}
